package utils;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.Calendar;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.primitives.Ints;
/**
 * Утилитный класс 
 * для всех утилит работы с ФС
 * @author Savva
 *
 */
public class BlockUtils {
	public static final int METADATOFFSET = 33;
	public static final int FILESIZEOFFSET = 29;
	public static final String ENCODESTRING = "UTF8";
	/**
	 * Получаем список указателей на файлы содержащие метаданные
	 * про файлы которые содержатся в каталоге 
	 * @param f
	 * @param root - место где хранятся физические блоки для доступа к данным
	 * @return
	 */
	public static List<File> getListFiles(File f, String root) {
		List<File> list = Lists.newArrayList();
		byte[] fileData = getFileData(f, root);
		if(fileData.length == 0)
			return list;
		ByteBuffer wrap = ByteBuffer.wrap(fileData);
		int pointer = wrap.getInt();
		while(pointer > 0) {
			list.add(getBlockByNumber(pointer, root));
			if(!wrap.hasRemaining())
				break;
			pointer = wrap.getInt();
		} 
		return list;
	}
	/**
	 * Получаем список указателей на файлы содержащие метаданные
	 * про файлы которые содержатся в каталоге 
	 * @param f
	 * @param root - место где хранятся физические блоки для доступа к данным
	 * @return
	 */
	private static List<Integer> getIntegerListFiles(File f, String root) {
		List<Integer> list = Lists.newArrayList();
		byte[] fileData = getFileData(f, root);
		if(fileData.length == 0)
			return list;
		ByteBuffer wrap = ByteBuffer.wrap(fileData);
		int pointer = wrap.getInt();
		while(pointer > 0) {
			list.add(pointer);
			if(!wrap.hasRemaining())
				break;
			pointer = wrap.getInt();
		} 
		return list;
	}
	/**
	 * Функция достаёт из метаданных файла его имя
	 * @param f
	 * @return
	 */
	public static String getFSName(File f) {
		FileInputStream fin = null;
		try {
			fin = new FileInputStream(f);
			byte[] read = new byte[(int) f.length()];
			try {
				fin.read(read);
				ByteBuffer wrap = ByteBuffer.wrap(read);
				wrap.getInt();
				byte[] stringByte = new byte[5];
				wrap.get(stringByte);
				fin.close();
				return new String(stringByte, ENCODESTRING);
			} catch (IOException e) {
				e.printStackTrace();
			}
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} 
		return null;
	}
	/**
	 * Получить из битовой карты номер пустого блока
	 * @param map
	 * @return
	 */
	public static Integer getNumberEmptyBlock(Map<Integer, Boolean> map) {
		for (Entry<Integer, Boolean> iterable_element : map.entrySet()) {
			if(iterable_element.getValue()) {
				iterable_element.setValue(false);
				return iterable_element.getKey();
			}
		}
		return null;
	}
	/**
	 * Пишет в пустой файл его метаданные: тип, имя, дату изменения и размер файла
	 * @param type
	 * @param name
	 * @param f
	 * @param size
	 * @return
	 */
	public static File writeToFile(FileType type, String name, File f, int size) {
		if (!f.canRead())
			try {
				throw new FileNotFoundException("Can't read file " + f.getAbsolutePath());
			} catch (FileNotFoundException e) {
				e.printStackTrace();
				System.exit(0);
			}
			long length = f.length();
			FileOutputStream fin = null;
			try {
				fin = new FileOutputStream(f);
			} catch (FileNotFoundException e) {
				e.printStackTrace();
			}
			byte[] b = new byte[(int) length];
			//Закидываем тип файла
			byte[] bytes = ByteBuffer.allocate(4).putInt(type.getValue()).array();
			Integer offset = 0;
			offset += BlockUtils.writeToArray(b, bytes, offset);
			if(name.length() > 5) {
				System.out.println("File name is too long " + name.length());
				clearBlock(f, length);
				System.exit(1);
			}
			//Записываем имя файла
			try {
				bytes = new byte[5];
				while(name.length() < bytes.length) 
					name += " ";
				bytes = name.getBytes(BlockUtils.ENCODESTRING);
			} catch (UnsupportedEncodingException e1) {
				e1.printStackTrace();
			}
			System.out.println(bytes.length);
			offset += BlockUtils.writeToArray(b, bytes, offset);
			//Записываем дату изменений
			offset = updateDate(b, offset);
			bytes = ByteBuffer.allocate(4).putInt(size).array();
			offset += BlockUtils.writeToArray(b, bytes, offset);
			int a = -1;
			bytes = ByteBuffer.allocate(4).putInt(a).array();
			for(int j = 0; j < bytes.length; j++) {
				b[b.length - j - 1] = bytes[j];
			}
			try {
				fin.write(b);
				fin.close();
			} catch (IOException e) {
				clearBlock(f, length);
				e.printStackTrace();
			}
			System.out.println(b);
			System.out.println("offset" + offset);
			return f;
	}
	/**
	 * Обновить дату
	 * @param b
	 * @param offset
	 * @return
	 */
	private static Integer updateDate(byte[] b, Integer offset) {
		byte[] bytes;
		Calendar instance = Calendar.getInstance();
		int day = instance.get(Calendar.DAY_OF_MONTH);
		int month = instance.get(Calendar.MONTH) + 1;
		int year = instance.get(Calendar.YEAR);
		int minutes = instance.get(Calendar.MINUTE);
		int hours = instance.get(Calendar.HOUR_OF_DAY);
		bytes = ByteBuffer.allocate(4).putInt(minutes).array();
		offset += BlockUtils.writeToArray(b, bytes, offset);
		bytes = ByteBuffer.allocate(4).putInt(hours).array();
		offset += BlockUtils.writeToArray(b, bytes, offset);
		bytes = ByteBuffer.allocate(4).putInt(day).array();
		offset += BlockUtils.writeToArray(b, bytes, offset);
		bytes = ByteBuffer.allocate(4).putInt(month).array();
		offset += BlockUtils.writeToArray(b, bytes, offset);
		bytes = ByteBuffer.allocate(4).putInt(year).array();
		offset += BlockUtils.writeToArray(b, bytes, offset);
		return offset;
	}
	/**
	 * Добавить указатель в метаданные папки
	 * @param f
	 * @param i
	 * @return
	 */
	public static File addCataloguePointer(File f, Integer i, String root) {
		List<Integer> integerListFiles = getIntegerListFiles(f, root);
		integerListFiles.add(i);
		FileInfo info = getFileInfo(f);
		reWriteToFile(info.getType(), info.getName(), f, integerListFiles);
		return f;
	}
	/**
	 * Удалить указатель в метаданные папки
	 * @param f
	 * @param i
	 * @return
	 */
	public static File removeCataloguePointer(File f, Integer i, String root) {
		List<Integer> integerListFiles = getIntegerListFiles(f, root);
		integerListFiles.remove(i);
		FileInfo info = getFileInfo(f);
		reWriteToFile(info.getType(), info.getName(), f, integerListFiles);
		return f;
	}
	/**
	 * Перезаписать метаданные о каталоге
	 * @param type
	 * @param name
	 * @param f
	 * @param pointersToFiles
	 * @return
	 */
	private static File reWriteToFile(FileType type, String name, File f, List<Integer> pointersToFiles) {
		if (!f.canRead())
			try {
				throw new FileNotFoundException("Can't read file " + f.getAbsolutePath());
			} catch (FileNotFoundException e) {
				e.printStackTrace();
				System.exit(0);
			}
			long length = f.length();
			FileOutputStream fin = null;
			try {
				fin = new FileOutputStream(f);
			} catch (FileNotFoundException e) {
				e.printStackTrace();
			}
			byte[] b = new byte[(int) length];
			//Закидываем тип файла
			byte[] bytes = ByteBuffer.allocate(4).putInt(type.getValue()).array();
			Integer offset = 0;
			offset += BlockUtils.writeToArray(b, bytes, offset);
			if(name.length() > 5) {
				System.out.println("File name is too long " + name.length());
				System.exit(1);
			}
			//Записываем имя файла
			try {
				bytes = new byte[5];
				while(name.length() < bytes.length) 
					name += " ";
				bytes = name.getBytes(BlockUtils.ENCODESTRING);
			} catch (UnsupportedEncodingException e1) {
				e1.printStackTrace();
			}
			offset += BlockUtils.writeToArray(b, bytes, offset);
			//Записываем дату изменений
			offset = updateDate(b, offset);
			bytes = ByteBuffer.allocate(4).putInt(pointersToFiles.size() * 4).array();
			offset += BlockUtils.writeToArray(b, bytes, offset);
			for(Integer i: pointersToFiles) {
				bytes = ByteBuffer.allocate(4).putInt(i).array();
				offset += BlockUtils.writeToArray(b, bytes, offset);
			}
			int a = -1;
			bytes = ByteBuffer.allocate(4).putInt(a).array();
			for(int j = 0; j < bytes.length; j++) {
				b[b.length - j - 1] = b[j];
			}
			try {
				fin.write(b);
				fin.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
			return f;
	}
	/**
	 * Найти блок по его номеру
	 * @param number
	 * @param root
	 * @return null если файл не найден
	 */
	public static File getBlockByNumber(Integer number, String root) {
		File f = new File(root);
		if (!f.canRead())
			try {
				throw new FileNotFoundException("Can't read file " + f.getAbsolutePath());
			} catch (FileNotFoundException e) {
				e.printStackTrace();
				System.exit(0);
			}
		File blob = null;
		File[] listFiles = f.listFiles();
		for(File file : listFiles) {
			if(file.getName().equalsIgnoreCase("file"+ number)) {
				blob = file;
				break;
			}
		}
		if(blob == null) {
			System.out.println("Can't find file system. You need init it first");
			System.exit(1);
		} 
		return blob;
	}
	/**
	 * Проверка существования пути до dest в моей ФС
	 * @param dest
	 * @return
	 */
	public static File checkDestToCatalogue(String root, String dest) {
		root += File.separator;
		//В начале итерации мы находимся в корне
		String[] split = dest.split(getSeparator());
		File f = BlockUtils.findSuperBlob(root);
		System.out.println(BlockUtils.getFileInfo(f));
		for(int i = 0; i <= split.length - 2; i++) {
				if(split[i].isEmpty())
					continue;
				boolean found = false;
				for(File files : BlockUtils.getListFiles(f, root)) {
					String fsName = BlockUtils.getFSName(files);
					if(fsName != null && split[i].trim().equalsIgnoreCase(fsName.trim())) {
						f = files;
						if(found) {
							System.out.println("There is another dir with same name");
							System.exit(1);
						}
							
						found = true;
					}
				}
				if(!found) {
					f = null;
					System.out.println("There is no such directory / " + split[i]+ " /");
					break;
				}
			}
		return f;
	}
	/**
	 * Проверка существования пути dest в моей ФС
	 * @param dest
	 * @return
	 */
	public static File checkDestFile(String root, String dest) {
		//В начале итерации мы находимся в корне
		String[] split = dest.split(getSeparator());
		File f = BlockUtils.findSuperBlob(root);
		for(int i = 0; i <= split.length - 1; i++) {
				if(split[i].isEmpty())
					continue;
				boolean found = false;
				for(File files : BlockUtils.getListFiles(f, root)) {
					String fsName = BlockUtils.getFSName(files);
					if(fsName != null && split[i].trim().equalsIgnoreCase(fsName.trim())) {
						f = files;
						found = true;
						break;
					}
				}
				if(!found) {
					f = null;
					System.out.println("There is no such directory or file / " + split[i]+ " /");
					break;
				}
			}
		return f;
	}
	/**
	 * Найти супер-блоб
	 * @param path
	 * @return
	 */
	public static File findSuperBlob(String path) {
		File f = new File(path);
		if (!f.canRead())
			try {
				throw new FileNotFoundException("Can't read file " + f.getAbsolutePath());
			} catch (FileNotFoundException e) {
				e.printStackTrace();
				System.exit(0);
			}
		File superBlob = null;
		File[] listFiles = f.listFiles();
		for(File file : listFiles) {
			if(file.getName().equalsIgnoreCase("file0")) {
				superBlob = file;
				break;
			}
		}
		if(superBlob == null) {
			System.out.println("Can't find file system. You need init it first");
			System.exit(1);
		} 
		return superBlob;
	}
	
	/**
	 * Вытащить номер блока по именит файла
	 * @param f
	 * @return
	 */
	public static Integer extractBlockNumber(File f) {
		String name = f.getName();
		if(!name.startsWith("file")) {
			System.out.println("cant extract block number from file " + name);
			return null;
		} else {
			String substring = name.substring(4);
			return Ints.tryParse(substring);
		}
	}
	/**
	 * Функция получения битовой карты 
	 * @param f - корень ФС.
	 * @return
	 */
	public static Map<Integer, Boolean> createBitMap(File f) {
		Map<Integer, Boolean> map = Maps.newHashMap();
		File[] files = f.listFiles(new FilenameFilter() {
			public boolean accept(File arg0, String arg1) {
				return arg1.substring(0, 4).equals("file");
			}
		});
		FileInputStream fin = null;
		byte[] b;
		for(int i = 1; i < files.length - 1; i++) {
			File currentFile = null;
			for(File file: files) {
				if(file.getName().equals("file" + i)) {
					currentFile = file;
					break;
				}
			}
			if(currentFile == null)
				continue;
			try {
				b = new byte[(int) currentFile.length()];
				if(b.length == 0)
					continue;
				fin = new FileInputStream(currentFile);
				try {
					fin.read(b);
				} catch (IOException e) {
					e.printStackTrace();
				}
				byte[] lastInt = new byte[4];
				for(int j = 0; j < lastInt.length; j++) {
					lastInt[j] = b[b.length - j - 1];
					
				}
				int int1 = ByteBuffer.wrap(lastInt).getInt();
				map.put(i, int1 == 0);
			} catch (FileNotFoundException e) {
				e.printStackTrace();
			}
		}
		return map;
	}
	/**
	 * Метаданные файла
	 * @param f
	 * @return
	 */
	public static FileInfo getFileInfo(File f) {
		FileInputStream fin = null;
		FileInfo info = null;
		try {
			fin = new FileInputStream(f);
			byte[] read = new byte[(int) f.length()];
			try {
				fin.read(read);
				if(read.length == 0) {
					System.out.println("File " + f.getName() + " is empty");
					System.exit(0);
				}
					
				info = new FileInfo();
				ByteBuffer wrap = ByteBuffer.wrap(read);
				int int1 = wrap.getInt();
				FileType type = FileType.getType(int1);
				if(type == null) {
					System.out.println("file isn't correct");
					System.exit(1);
				}
				info.setType(type);
				byte[] stringByte = new byte[5];
				wrap.get(stringByte);
				int1 = wrap.getInt();
				int minutes = int1;
				int1 = wrap.getInt();
				int hours = int1;
				int1 = wrap.getInt();
				int day = int1;
				int1 = wrap.getInt();
				int month = int1 - 1;
				int1 = wrap.getInt();
				int year = int1;
				Calendar instance = Calendar.getInstance();
				instance.set(year, month, day, hours, minutes);
				info.setDate(instance.getTime());
				info.setName(new String(stringByte, ENCODESTRING));
				info.setSize(wrap.getInt());
				fin.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} 
		return info;
	}
	/**
	 * Добавить в массив данные с заданным смещением 
	 * @param destArray
	 * @param hostArray
	 * @param offset
	 * @return
	 */
	public static int writeToArray(byte[] destArray, byte[] hostArray,
			Integer offset) {
		int addedoffset = 0;
		for(int i = 0; i < hostArray.length; i++) {
			destArray[i + offset] = hostArray[i];
			addedoffset++;
		}
		return addedoffset;
	}
	
	/**
	 * Подготовить path корня
	 * @param root
	 * @return
	 */
	public static String prepareRoot(String root) {
		if(!root.endsWith(getSeparator()))
			root += getSeparator();
		return root;
	}
	/**
	 * Подготовить path для работы внутри системы
	 * @param dest
	 * @param root
	 * @return
	 */
	public static String prepareDest(String dest, String root) {
		if(!(dest.startsWith("\\\\") || dest.startsWith("/"))) {
			dest = dest.substring(root.length() - 1);
			System.out.println(dest);
		}
		String anObject = "\\\\";
		String replacement = "/";
		if(getSeparator().equals(anObject))
			dest = dest.replaceAll(replacement, anObject);
		else
			dest = dest.replaceAll(anObject, replacement);
		return dest;
	}
	
	/**
	 * Получить размер файла
	 * @param f
	 * @return
	 */
	public static int getFileFSSize(File f) {
		FileInputStream fin = null;
		int a = 0;
		try {
			fin = new FileInputStream(f);
			long length = f.length();
			byte[] b = new byte[(int) length];
			fin.read(b);
			a = ByteBuffer.wrap(b, FILESIZEOFFSET, 4).getInt();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			try {
				fin.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		return a;
	}
	/**
	 * Разделитель системы
	 * @return
	 */
	public static String getSeparator() {
		String separator = null;
		if(File.separator.equals("\\"))
			separator = "\\\\";
		else
			separator = "/";
		return separator;
	}
	/**
	 * Установить в файле следующий блок
	 * @param b
	 * @param number
	 */
	public static void setNextBlock(byte[] b, int number) {
		byte[] bytes = ByteBuffer.allocate(4).putInt(number).array();
		for(int j = 0; j < bytes.length; j++) {
			b[b.length - j - 1] = bytes[j];
		}
	}
	/**
	 * Очистить блок (заполнить нулями)
	 * @param f
	 * @param length
	 */
	public static void clearBlock(File f, long length) {
		byte[] bs = new byte[(int) f.length()];
		//Инициализируем нулями все файлы созданной ФС
		for(int i =0; i < length - 1; i++) {
			bs[i] = 0;
		}
		try {
			for(int i = 0; i < f.length(); i++) {
				FileOutputStream fout = new FileOutputStream(f);
				fout.write(bs);
				fout.close();
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * 
	 * @param array
	 * @return > 0 если есть следующий блок
	 */
	public static int getNextFile(byte[] array) {
		byte[] subarray = new byte[4];
		int j = 0;
		for(int i = array.length - 1; i > array.length - 5; i--) {
			subarray[j] = array[i];
			j++;
		}
		return ByteBuffer.wrap(subarray).getInt();
	}
	/**
	 * Получить все данные файла (учитывая все блоки которые используются)
	 * @param f
	 * @param root
	 * @return
	 */
	public static byte[] getFileData(File f, String root) {
		File currentFile = f;
		int fileFSSize = getFileFSSize(f);
		byte[] b = new byte[fileFSSize];
		int currentoffset = 0;
		boolean isFirstFile = true;
		FileInputStream fin = null;
		try {
			while(currentFile != null) {
				if(isFirstFile) {
					int length = (int) currentFile.length();
					byte[] b1 = new byte[length];
					fin = new FileInputStream(currentFile);
					fin.read(b1);
					for(int i = METADATOFFSET; i < length - 4; i++, currentoffset++) {
						if(b.length - 1 < currentoffset )
							return b;
						b[currentoffset] = b1[i];
					}
					int number = getNextFile(b1);
					if(number > 0)
						currentFile = getBlockByNumber(number, root);
					else 
						currentFile = null;
					isFirstFile = false;
				} else {
					int length = (int) currentFile.length();
					byte[] b1 = new byte[length];
					fin = new FileInputStream(currentFile);
					fin.read(b1);
					for(int i = 0; i < length - 4; i++, currentoffset++) {
						if(b.length - 1 < currentoffset)
							return b;
						b[currentoffset] = b1[i];
					}
					int number = getNextFile(b1);
					if(number > 0)
						currentFile = getBlockByNumber(number, root);
					else 
						currentFile = null;
				}
			}
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			if(fin != null)
				try {
					fin.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
		}
		return b;
	}
}

